iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0

Mutex(互斥鎖)、Condition Variables(條件變量)和Semaphore(信號量)是多線程編程中常用的同步原語,它們各自有不同的功能,但也可以相互配合使用。以下是它們之間的關係和主要區別:

1. Mutex(互斥鎖)

  • 功能:確保同一時間只有一個線程可以訪問共享資源,防止競爭條件。
  • 使用方式:線程在訪問共享資源之前必須獲取互斥鎖,訪問完後釋放鎖。
  • 特點:只允許一個線程持有鎖,其他線程必須等待。

2. Condition Variables(條件變量)

  • 功能:允許線程在某個條件不滿足時進入等待狀態,並在條件滿足時被喚醒。
  • 使用方式:通常與互斥鎖一起使用。線程在檢查條件時需要先獲取互斥鎖,然後根據條件決定是否等待。
  • 特點:不直接控制資源的訪問,而是提供一種機制來協調線程之間的執行。

3. Semaphore(信號量)

  • 功能:用於控制對共享資源的訪問,可以管理多個資源的併發訪問。
  • 使用方式:線程在訪問資源之前進行等待操作,使用後釋放資源。計數信號量允許多個線程同時訪問。
  • 特點:可以用於限制同時訪問的線程數量,並且不一定需要互斥鎖。

三者的關係

  • 互補性:這三者可以一起使用,以實現更複雜的同步需求。例如,可以使用互斥鎖來保護共享資源的訪問,使用條件變量來在某些條件下讓線程等待,使用信號量來控制同時訪問的線程數量。

  • 典型用法

    • 在生產者-消費者問題中,互斥鎖保護共享緩衝區的訪問,條件變量用於生產者和消費者之間的協調,而信號量則可以用來限制緩衝區的大小。

總結

  • Mutex 用於保護共享資源的訪問。
  • Condition Variables 用於線程之間的協調和通信。
  • Semaphore 用於控制對多個資源的訪問。

以下是 MutexCondition VariablesSemaphore 的獨立範例程式碼。

1. Mutex 範例

// mutex.cpp
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int sharedResource = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        mtx.lock(); // 獲取鎖
        ++sharedResource; // 訪問共享資源
        mtx.unlock(); // 釋放鎖
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Final value: " << sharedResource << std::endl; 
    // 因為兩個線程各自增加了 10000 次,所以是 20000。
    return 0;
}

https://ithelp.ithome.com.tw/upload/images/20241008/2016876627tVYNkuN8.png
使用互斥鎖來保護共享資源,避免競爭條件,並使用兩個線程同時增加同一個變量的值。

2. Condition Variables 範例

// cv.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::queue<int> buffer;
std::mutex mtx;
std::condition_variable cv;

void producer() {
    for (int i = 0; i < 10; ++i) {
        {
            std::lock_guard<std::mutex> lock(mtx);
            buffer.push(i);
            std::cout << "Produced: " << i << std::endl;
        }
        cv.notify_one(); // 通知消費者
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return !buffer.empty(); }); // 等待條件變量
        int item = buffer.front();
        buffer.pop();
        std::cout << "Consumed: " << item << std::endl;
    }
}

int main() {
    std::thread prodThread(producer);
    std::thread consThread(consumer);

    prodThread.join();
    consThread.join();

    return 0;
}

https://ithelp.ithome.com.tw/upload/images/20241008/20168766Af8siDGDGn.png
這段程式碼實現了一個使用條件變量的生產者-消費者模型。與之前的例子相比,這裡使用了 std::condition_variable 來更有效地處理生產者和消費者之間的同步。以下是程式碼的詳細解釋:

主要組件

  1. 標頭檔

    • #include <iostream>:用於輸入輸出。
    • #include <thread>:用於多線程支持。
    • #include <mutex>:用於互斥鎖,保護共享資源。
    • #include <condition_variable>:用於條件變量,實現線程間的通知。
    • #include <queue>:用於佇列資料結構。
  2. 全域變數

    • std::queue<int> buffer;:用於存儲生產的資料。
    • std::mutex mtx;:互斥鎖,用於保護對佇列的訪問。
    • std::condition_variable cv;:條件變量,用於通知消費者有資料可消費。

主要函數

  1. producer()

    • 生產者函數,負責生成資料並將其放入佇列中。
    • 在每次生成資料時,使用 std::lock_guard<std::mutex> lock(mtx); 獲取鎖,然後將資料放入佇列。
    • 使用 cv.notify_one(); 通知消費者有新資料可用。
    • 每次生產後,暫停 100 毫秒,模擬生產的延遲。
  2. consumer()

    • 消費者函數,負責從佇列中取出資料。
    • 使用 std::unique_lock<std::mutex> lock(mtx); 獲取鎖,然後使用 cv.wait(lock, [] { return !buffer.empty(); }); 等待條件變量,直到佇列不再空。
    • 一旦有資料可消費,消費者從佇列中取出資料並將其打印出來。

整體流程

  1. 生產者開始運行,生成資料並將其放入佇列中。
  2. 每當生產者生成一個項目後,會通知消費者有資料可供消費。
  3. 消費者在沒有資料時會阻塞,直到收到生產者的通知。
  4. 當消費者收到通知後,它會檢查佇列是否有資料,然後消費資料。

優點

  • 效率:使用條件變量可以避免消費者在佇列為空時不斷輪詢,這樣可以節省 CPU 資源。
  • 簡潔性:程式碼結構清晰,容易理解。

注意事項

  • 條件變量的使用cv.wait(lock, [] { return !buffer.empty(); }); 是一個條件等待,只有當 buffer 非空時,消費者才會從等待中醒來。
  • 互斥鎖:在進入臨界區時,確保正確使用互斥鎖以防止競爭條件。

總結

這段程式碼展示了如何使用 C++ 的多線程功能和條件變量來實現一個高效的生產者-消費者模型。生產者和消費者之間的協調通過條件變量來實現,這樣可以提高資源的使用效率。

3. Semaphore 範例

// semaphore.cpp
#include <iostream>
#include <thread>
#include <semaphore.h>
#include <queue>
#include <mutex>
#include <chrono>

const int MAX_QUEUE_SIZE = 5;
std::queue<int> buffer;
std::mutex mtx;
sem_t empty; // 用於表示空位的信號量
sem_t full;  // 用於表示佇列中項目的信號量

void producer() {
    std::cout << "Producer starting..." << std::endl;
    for (int i = 0; i < 10; ++i) {
        sem_wait(&empty); // 等待有空位
        {
            std::lock_guard<std::mutex> lock(mtx);
            buffer.push(i);
            std::cout << "Produced: " << i << std::endl;
        }
        sem_post(&full); // 增加佇列中項目的計數
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    std::cout << "Consumer starting..." << std::endl;
    for (int i = 0; i < 10; ++i) {
        sem_wait(&full); // 等待有項目可消費
        {
            std::lock_guard<std::mutex> lock(mtx);
            int item = buffer.front();
            buffer.pop();
            std::cout << "Consumed: " << item << std::endl;
        }
        sem_post(&empty); // 增加空位的計數
        std::this_thread::sleep_for(std::chrono::milliseconds(50));
    }
}

int main() {
    sem_init(&empty, 0, MAX_QUEUE_SIZE); // 初始化空位信號量
    sem_init(&full, 0, 0); // 初始化佇列中項目的信號量為0

    std::thread prodThread(producer);
    std::thread consThread(consumer);

    prodThread.join();
    consThread.join();

    sem_destroy(&empty); // 銷毀信號量
    sem_destroy(&full); // 銷毀信號量
    return 0;
}

https://ithelp.ithome.com.tw/upload/images/20241008/20168766ShvjX9FTuf.png
這段程式碼實現了一個簡單的生產者-消費者模型,使用 C++ 的多線程功能來模擬生產者生成資料並將其放入佇列,消費者則從佇列中取出資料進行消費。以下是程式碼的詳細說明:

主要組件

  1. 標頭檔

    • #include <iostream>:用於輸入輸出。
    • #include <thread>:用於多線程支持。
    • #include <semaphore.h>:用於信號量的操作。
    • #include <queue>:用於佇列資料結構。
    • #include <mutex>:用於互斥鎖,保護共享資源。
    • #include <chrono>:用於時間相關的功能。
  2. 全域變數

    • const int MAX_QUEUE_SIZE = 5;:佇列的最大容量。
    • std::queue<int> buffer;:用於存儲生產的資料。
    • std::mutex mtx;:互斥鎖,用於保護對佇列的訪問。
    • sem_t empty;:信號量,用於表示佇列中的空位。
    • sem_t full;:信號量,用於表示佇列中已有的項目。

主要函數

  1. producer()

    • 生產者函數,負責生成資料並將其放入佇列中。
    • 在每次生成資料前,使用 sem_wait(&empty); 等待有空位。
    • 進入臨界區後,使用 std::lock_guard<std::mutex> lock(mtx); 獲取鎖,然後將資料放入佇列。
    • 生成後,使用 sem_post(&full); 增加佇列中項目的計數。
    • 每次生成後,暫停 100 毫秒,模擬生產的延遲。
  2. consumer()

    • 消費者函數,負責從佇列中取出資料。
    • 在每次消費前,使用 sem_wait(&full); 等待有項目可消費。
    • 進入臨界區後,使用 std::lock_guard<std::mutex> lock(mtx); 獲取鎖,然後從佇列中取出資料。
    • 消費後,使用 sem_post(&empty); 增加空位的計數。
    • 每次消費後,暫停 50 毫秒,模擬消費的延遲。
  3. main()

    • 初始化信號量:
      • sem_init(&empty, 0, MAX_QUEUE_SIZE);:設定空位信號量的初始值為最大佇列容量。
      • sem_init(&full, 0, 0);:設定佇列中項目的信號量初始值為 0。
    • 創建生產者和消費者的執行緒:
      • std::thread prodThread(producer);
      • std::thread consThread(consumer);
    • 使用 join() 等待兩個執行緒完成。
    • 銷毀信號量以釋放資源。

整體流程

  1. 生產者開始運行,生成資料並將其放入佇列中。
  2. 消費者在有資料可消費時運行,取出佇列中的資料。
  3. 使用信號量和互斥鎖確保生產者和消費者之間的協調,防止佇列溢出或空佇列的情況。

注意事項

  • 同步與互斥:使用信號量來控制生產者和消費者之間的同步,使用互斥鎖來保護對共享資源(佇列)的訪問。
  • 延遲:使用 std::this_thread::sleep_for 來模擬生產和消費的延遲,這樣可以更清楚地看到生產和消費的過程。

這段程式碼是一個經典的生產者-消費者問題的實現,展示了如何使用多線程和信號量來解決資源共享的問題。

範例總結:

  1. Mutex 範例:這個範例展示了如何使用 Mutex 來保護對共享資源的訪問,確保兩個線程同時增量共享變量不會導致競爭條件。

  2. Condition Variables 範例:這個範例展示了生產者和消費者之間的協作,使用條件變量來等待和通知。

  3. Semaphore 範例:這個範例展示了如何使用信號量來限制同時訪問的資源數量,確保不會超過緩衝區的容量。


上一篇
ch6-同步之硬體 Hardware Support
下一篇
ch6-哲學家進餐問題(Dining Philosophers Problem)
系列文
十年後重讀作業系統恐龍本30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言